In [1]:
%pip install transliterate
Requirement already satisfied: transliterate in /home/olena/myvenv/lib/python3.12/site-packages (1.10.2)
Requirement already satisfied: six>=1.1.0 in /home/olena/myvenv/lib/python3.12/site-packages (from transliterate) (1.17.0)
Note: you may need to restart the kernel to use updated packages.
In [2]:
%pip install rapidfuzz
Requirement already satisfied: rapidfuzz in /home/olena/myvenv/lib/python3.12/site-packages (3.13.0)
Note: you may need to restart the kernel to use updated packages.
In [3]:
import pandas as pd
import numpy as np
import altair as alt
import matplotlib.pyplot as plt
from transliterate import translit
from rapidfuzz import process, fuzz
In [4]:
df2_1_lyst = pd.read_csv('2.1_Дунаєць.xlsx - База листов.csv')
df2_1_people = pd.read_csv('2.1_Дунаєць.xlsx - База людей.csv')
df2_2_base = pd.read_csv('2.2_Дунаєць_1778.xlsx - База.csv')
In [5]:
#Статеве співвідношення різних вікових груп, як змінилося за сто років

#1 датасет
#Очистка даних

#Вік
#Спочатку перекладаємо англіською колонку "Возраст" і перевіряємо, чи всі значення числові
df2_1_people = df2_1_people.rename(columns={'Возраст': 'Age'})
mask = df2_1_people['Age'].astype(str).str.contains(r'[a-zA-Z]', na=False)
df2_1_people[mask]
Out[5]:
Кто заполнял базу ID строки в базе List ID Link ID Домохозяйствао ID жилец ФИО Пол Глава хозяйства и глава семьи Age ... Здесь ли обыкновенно проживает Отметка об отсуствии Вероисповедание Родной язык Умеет ли читать Обучение Профессия главное Профессия вспомогательное Положение по воинской повинности Примітки
42 Podhorna 43.0 F.132.Op.1.Spr.1343. Арк.13-14 https://www.familysearch.org/ark:/61903/3:1:3Q... 6.0 11.0 Lukash Georgij Mikhajlov m grandson/son 2 months ... 1 NaN orthodox ukrm 0.0 NaN with father NaN NaN NaN
89 Podhorna 90.0 F.132.Op.1.Spr.1343. Арк.33-34 https://www.familysearch.org/ark:/61903/3:1:3Q... 16.0 5.0 Sachkova Mariya Ivanova f daughter 6 months ... 1 NaN orthodox ukrm 0.0 NaN with parents NaN NaN NaN
141 Podhorna 142.0 F.132.Op.1.Spr.1343. Арк.53-54 https://www.familysearch.org/ark:/61903/3:1:3Q... 26.0 5.0 Davidenko Daniil Vasiliev m son 6 months ... 1 NaN orthodox ukrm 0.0 NaN with father NaN NaN NaN
165 Podhorna 166.0 F.132.Op.1.Spr.1343. Арк.61-62 https://www.familysearch.org/ark:/61903/3:1:3Q... 30.0 6.0 Sapozhnikov Andrej Zakhariev m grandson/son 4 months ... 1 NaN orthodox ukrm 0.0 NaN with father NaN NaN NaN
189 Podhorna 190.0 F.132.Op.1.Spr.1343. Арк.69-70 https://www.familysearch.org/ark:/61903/3:1:3Q... 34.0 4.0 Kontribuc Ivan Savvin m son 2 months ... 1 NaN orthodox ukrm 0.0 NaN with father NaN NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1527 Podhorna 1528.0 F.132.Op.1.Spr.1343. Арк.544-545 https://www.familysearch.org/ark:/61903/3:1:3Q... 258.0 6.0 Belyavskaya Varvara Ivanova f daughter 2 months ... 1 NaN orthodox ukrm 0.0 NaN with parents NaN NaN NaN
1548 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1549 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1550 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1551 * - В оригінальному джерелі ячейка "мова" була... NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

69 rows × 25 columns

In [6]:
#Приходимо до висновку, що деякий вік записаний не в роках, а у деяких клітинках NaN значення, що нам не підходить
#Оскільки статистично нам немає різниці, скільки конкретно місяців/тижнів/днів дитині, записуємо як 0 років
df2_1_people = df2_1_people.replace(to_replace=r'.*month.*', value=0, regex=True)
df2_1_people = df2_1_people.replace(to_replace=r'.*days.*', value=0, regex=True)
df2_1_people = df2_1_people.replace(to_replace=r'.*weeks.*', value=0, regex=True)

#Останні 4 рядки не містять даних, їх видаляємо
df2_1_people = df2_1_people.iloc[:-4]

#Залишилось 3 рядка із невідомим віком - на жаль, в інших таблицях не зазначено інформації про вік, тому ці 3 випадки виключаємо
df2_1_people_with_age = df2_1_people.drop(index=[987, 988, 989])
In [7]:
df2_1_people_with_age['Age'] = pd.to_numeric(df2_1_people_with_age['Age'], errors='coerce').astype('float64')
In [8]:
df2_1_people_with_age.Age.describe()
Out[8]:
count    1545.000000
mean       25.423625
std        19.493821
min         0.000000
25%         9.000000
50%        23.000000
75%        39.000000
max        90.000000
Name: Age, dtype: float64
In [9]:
#дані чисті - вік що 0, що 90 не є аномальними, тому вони важливі для статистики
In [10]:
#Стать

#Перевірка
df2_1_people = df2_1_people.rename(columns={'Пол': 'Sex'})
df2_1_people[~df2_1_people['Sex'].isin(['f', 'm'])]

#Ті самі винятки, що і були з віком
df2_1_lyst.columns
Out[10]:
Index(['Кто заполнял базу', 'List ID', 'Link', 'ID Страница',
       'ID Домохозяйствао', 'Губерния', 'Уезд', 'Волость ', 'Село/деревня',
       'Переписной участок', 'Счетный участок', 'Стан или полицейский участок',
       'Хозяин', 'Сколько жилых строений', 'Строение построено', 'Чем крыто',
       'Всего наличного мужеского населения',
       'Всего наличнаго женского населения', 'Постоянно живущаго М',
       'Постоянно живущаго Ж', 'Некрестьянсокго сословия М',
       'Некрестьянского сословия Ж', 'Приписанного здесь М',
       'Приписанного здесь Ж', 'Подпись счетчика'],
      dtype='object')
In [11]:
#У таблиці 2.1 База листов є дані про 'Всего наличнаго женского населения' і 'Всего наличного мужеского населения', що і використаємо для статистики
df_merged = pd.merge(df2_1_people, df2_1_lyst, on='ID Домохозяйствао')
df_merged[df_merged['Age'].isna()]
Out[11]:
Кто заполнял базу_x ID строки в базе List ID_x Link_x ID Домохозяйствао ID жилец ФИО Sex Глава хозяйства и глава семьи Age ... Чем крыто Всего наличного мужеского населения Всего наличнаго женского населения Постоянно живущаго М Постоянно живущаго Ж Некрестьянсокго сословия М Некрестьянского сословия Ж Приписанного здесь М Приписанного здесь Ж Подпись счетчика
987 Podhorna 988.0 F.132.Op.1.Spr.1343. Арк.355-356 NaN 169.0 NaN NaN NaN NaN NaN ... straw 1.0 2.0 1.0 2.0 0.0 0.0 1.0 2.0 Ivan Belyavskij
988 Podhorna 989.0 F.132.Op.1.Spr.1343. Арк.355-356 NaN 169.0 NaN NaN NaN NaN NaN ... straw 1.0 2.0 1.0 2.0 0.0 0.0 1.0 2.0 Ivan Belyavskij
989 Podhorna 990.0 F.132.Op.1.Spr.1343. Арк.355-356 NaN 169.0 NaN NaN NaN NaN NaN ... straw 1.0 2.0 1.0 2.0 0.0 0.0 1.0 2.0 Ivan Belyavskij

3 rows × 49 columns

In [12]:
#Усі ці 3 винятки з одного господарства, в якому 1 чоловік і 2 жінки

df2_1_people.loc[987, 'Sex'] = 'm'
df2_1_people.loc[988, 'Sex'] = 'f'
df2_1_people.loc[989, 'Sex'] = 'f'
#замінили NaN значеннями вручну
#варто зазначити, що для статистики початково не брались стовпці із господарствами тому, що NaN значення у них значно переважали той, з яким я працювала
df2_1_people[~df2_1_people['Sex'].isin(['f', 'm'])]
Out[12]:
Кто заполнял базу ID строки в базе List ID Link ID Домохозяйствао ID жилец ФИО Sex Глава хозяйства и глава семьи Age ... Здесь ли обыкновенно проживает Отметка об отсуствии Вероисповедание Родной язык Умеет ли читать Обучение Профессия главное Профессия вспомогательное Положение по воинской повинности Примітки

0 rows × 25 columns

In [13]:
#2 датасет
df2_2_base_processed = df2_2_base.copy()

df2_2_base_processed.columns = df2_2_base_processed.iloc[2]
df2_2_base_processed = df2_2_base_processed.iloc[3:]
df2_2_base_processed = df2_2_base_processed.reset_index(drop=True)
df2_2_base_processed.columns.values[0] = 'Аркуш'
df2_2_base_processed.columns.values[4] = 'Чоловіки наскрізне'
df2_2_base_processed.columns.values[5] = 'Жінки наскрізне'
df2_2_base_processed.columns.values[6] = 'Чоловіки джерело'
df2_2_base_processed.columns.values[7] = 'Жінки джерело'

#виправлено назви стовпців
df2_2_base_processed
Out[13]:
2 Аркуш ID наскрізна ID домогосподарство Двори джерело Чоловіки наскрізне Жінки наскрізне Чоловіки джерело Жінки джерело Імʼя По-батькові Прізвище Родиний статус Категорія Клас Соціальний статус Вік Був на сповіді Не був Не був за малолітством
0 445 1 1 1 1 NaN 1 NaN Федор Кондратьев Лукашевич господар nuclear 3b духовные 44 х NaN NaN
1 NaN 2 NaN NaN NaN 1 NaN 1 Варвара Симева NaN жена NaN NaN духовные 35 х NaN NaN
2 NaN 3 NaN NaN NaN 2 NaN 2 Анна NaN NaN дочь NaN NaN духовные 17 х NaN NaN
3 NaN 4 NaN NaN 2 NaN 2 NaN Тимофей NaN NaN сын NaN NaN духовные 15 х NaN NaN
4 NaN 5 NaN NaN 2 NaN 2 NaN Пантелеймон NaN NaN сын NaN NaN духовные 14 х NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1592 NaN 1593 104 12 824 NaN 236 NaN Леонтий Афанасиев NaN двоюродный брат Александра nuclear 3a бездворные 25 х NaN NaN
1593 NaN 1594 NaN NaN NaN 770 NaN 222 Мотрона Михайлова NaN жена его NaN NaN бездворные 22 х NaN NaN
1594 NaN 1595 105 13 825 NaN 237 NaN Герасим Фомик Мерник господар nuclear 3b бездворные 43 х NaN NaN
1595 NaN 1596 NaN NaN NaN 771 NaN 223 Агафия Максимова NaN жена его NaN NaN бездворные 42 х NaN NaN
1596 NaN 1597 NaN NaN 826 NaN 238 NaN Лукьян NaN NaN NaN NaN NaN бездворные 14 х NaN NaN

1597 rows × 19 columns

In [14]:
df2_2_base_processed['Age'] = df2_2_base_processed['Вік']
mask = df2_2_base_processed['Age'].astype(str).str.contains(r'[a-zA-Z]', na=False)
df2_2_base_processed[mask]
Out[14]:
2 Аркуш ID наскрізна ID домогосподарство Двори джерело Чоловіки наскрізне Жінки наскрізне Чоловіки джерело Жінки джерело Імʼя По-батькові Прізвище Родиний статус Категорія Клас Соціальний статус Вік Був на сповіді Не був Не був за малолітством Age
In [15]:
df2_2_base_processed['Age'] = pd.to_numeric(df2_2_base_processed['Age'], errors='coerce').astype('float64')
df2_2_base_processed.Age.describe()
Out[15]:
count    1597.000000
mean       23.529743
std        16.608718
min         1.000000
25%         9.000000
50%        21.000000
75%        35.000000
max        84.000000
Name: Age, dtype: float64
In [16]:
#знову немає аномалій
In [17]:
#оскільки наскрізні дані і дані джерела занадто сильно різняться, стать краще опрацювати через родинний статус
df2_2_base_processed['Родиний статус'].unique()
Out[17]:
array(['господар', 'жена', 'дочь', ' сын', 'сын', 'дячок',
       'брат Феодосия', 'жена Савы', 'жена Григория', 'жена Кирила',
       'господар вдов', 'жена Алексеея', 'сын Алексея', 'дочь Алексея',
       'жена Антона', 'сын Антона', 'дочь Антона', 'жена Андрея',
       'сын Андрея', 'дочь Андрея', 'жена Стефана', 'дочь стефана',
       'племянник Кузьмы вдов', 'сын Герасима', 'жена Ивана',
       'жена Никиты', 'сын Никиты', 'жена Максима', 'племянник Герасима',
       'племянница Герасима', 'невестка Кузьмы вдова', 'сын Федоры',
       'жена Павла', 'сын Павла', 'жена Леонтия', 'племянник Феодосии',
       'жена Герасима', 'дочь Герасима', 'племянник Кузьмы',
       'Федора жена', 'дочь Федора', 'сын Федора', 'жена Мирона',
       'сын Мирона', 'жена Ивана Мойсеева', 'дочь их', 'сын их',
       'племянник Ивана', 'жена его', 'сын его', 'жена Василия',
       'брат Федора', 'жена Терентия', 'дядина Федора удова',
       'сын Зиновии', 'дочь Зиновии', 'племяннкк Фомы', 'доч их',
       'брат Конона', 'зать Мирона', 'невестка Конона вдова',
       'сын Екатерины', 'дочь Екатерины', 'дядя Конона', 'жена Бориса',
       'жена Романа', 'дочь Романа', 'жена Гаврила', 'дочь Гаврила',
       'жена Якова', 'брат Никиты', 'жена Федора', 'жена Тараса',
       'брат Ивана', 'жена Ефима', 'жена Никифора', 'жена Онисима',
       'зять Прокопа', 'брат Прокопа', 'невестка Прокопа вдова', 'сын ее',
       'дочь ее', 'жена Филипа', 'жена Родиона', 'брат Захария',
       'жена Михаила', 'брат двоюродный Захария', 'жена Сазона', nan,
       'дядина Захария вдова', 'жена Трофима', 'жена  Николая',
       'жена Петрова', 'жена Тимофея', 'брат Каленика',
       'дядя Каленика вдов', 'племянник Каленика',
       'племянник Каленика вдов', 'жена Прокопа', 'зять Конона',
       'племянник Конона', 'двоюродный брат Федора', 'жена Емельяна',
       'невестка Никона вдова', 'зять Ефимия', 'племянник Никона',
       'жена Кондрата', 'жена Григоря', 'жена Дионисия', 'брат Харитона',
       'зять Леонтия', 'зять Алексея', 'племянник Артема',
       'двоюродный брат Артема', 'братов племянник', 'зять Гаврила',
       'невестка братов удова', 'жена Марка', 'зять Зиновии',
       'двоюродный брат Зиновии', 'брат Кондрата', 'племянник братов',
       'жена Дмитрия', 'жена Климентия', 'племянник Петра',
       'жена Ермолая', 'племянник Фтеодора', 'жена Петра', 'жена Артема',
       'зять Артема', 'племянник Елисея', 'племянница Елисея',
       'жена Евтихия', 'сын их вдов', 'дочь Еремея', 'жена Корнилия',
       'брат Иоакима', 'двоюродный брат Кореня', 'жена Якима', '?',
       'брат Терентия', 'двоюродный брат Демьяна', 'брат Игната',
       'жена Сидора', 'двоюродный брат Ивана', 'дочь его', 'зять их',
       'жена Уласа', 'зять Терентия', 'зать Устины', 'жена Матвея',
       'невестка Филона вдова', 'невестка Евсевия вдова',
       'племянник Евсея', 'племянник Максима', 'жена Иосифа',
       'жена Гордея', 'двоюродный брат Стефана', 'брат Якова',
       'господар вдова', 'жена Потапа', 'племянник Агафии', 'жена Конона',
       'жена Фомы', 'невестка Акилины вдова', 'сын ее вдов',
       'жена Семена', 'дочь Якова', 'невестка Ивана вдова', 'гоподар',
       'двоюродный брат Лазара', 'брат Антона', 'племянник Антона',
       'жена Нестора', 'племянница Антона вдова', 'брат Тараса',
       'двоюродный дядя Тараса', 'жена Клима', 'брат Логвина',
       'племянник Логвина', 'жена Марко', 'брат Кузьмы',
       'племянник Клима', 'жена Сергея', 'жена Алексея', 'зять Федора',
       'зять Семена вдов', 'двоюродный дядя Семена',
       'племянник Параскеви', 'брат Леонтия', 'сестра Леонтия',
       'брат Василия', 'двоюродный барт Василия', 'плеянник Федора',
       'зять Кирила', 'жена Никона', 'брат Емельяна', 'племянник Семена',
       'племянница Семена', 'онук Родиона', 'племянник Родиона',
       'племянник Афанасия', 'двоюродный брат Афанасия',
       'дядина Конона вдова', 'жена Лаврентия', 'двоюродный брат Конона',
       'двоюродный брат Василия', 'племянник Наталии вдов',
       'невестка Кирила вдова', 'жена Кузьмы', 'двоюродный брат Кирила',
       'мать Андрея вдова', 'племянниа Андрея вдова', 'племянник Андрея',
       'двоюродный брат Андрея', 'дядя двоюродный Андрея',
       'двоюродный брат Федора вдов',
       'двоюродный брат Федора и родной брат Касьяна', 'племянник Никиты',
       'брат Савы', 'дядина Савы вдова', 'жена Давида', 'зять Любови',
       'жена Мойсея', 'племянник Павла', 'племянница Павла',
       'дядя двоюродный Павла', 'невестка Дмитрия вдова', 'девушка',
       'удова ключница', 'староста', 'повар', 'кухарка вдова',
       'иконописец', 'кузнец', 'охотник', 'ученик', 'сапожник',
       'садовник', 'брат Иосифа', 'дворник', 'гончар', 'конюх', 'кучер',
       'брат Трофима', 'сестра Трофима', 'дядина Трофима удова',
       'племянник Григория', 'невестка Василия удова',
       'невестка Григория удова', 'жена Касьяна', 'племянник Иосифа',
       'двоюродный брат Никиты удов ', 'брат Марка', 'теща Ефима удова',
       'теща Симеона удова', 'двоюродный брат Симеона', 'зять Василия',
       'зять Романа', 'зать Михайла', 'жена Симеона',
       'двоюродный брат Савы', 'зять Павла', 'швагер Федора',
       'сестра его', 'дядина Павла', 'жена Гордиева', 'господар удов',
       'брат Григория', 'жена Алексеяя', 'жена Захария', 'сосед Ивана',
       'імовірно голова іншої родини', 'брат Еремея',
       'мать Василия удова', 'дядина Василия удова', 'жена Федова',
       'брат Симеона', 'невестка Симеона', 'жена Карпа', 'брат Михайла',
       'племянник Михайла', 'швагер Константина', 'брат Никифора',
       'жена Евсевия', 'двоюродный брат Харитона', 'гоподар удов',
       'жена Елельяна', 'теща Василия удова', 'господар удова',
       'жена Гавриила', 'жена Александра', 'двоюродный брат Александра'],
      dtype=object)
In [18]:
def guess_sex(status):
    if pd.isna(status):
        return pd.NA
    status = status.lower().strip()

    male_keywords = [
        'господар', 'сын', 'брат', 'дядя', 'зять', 'швагер', 'гончар', 'староста', 'кузнец',
        'сосед', 'кучер', 'конюх', 'садовник', 'дворник', 'охотник', 'племянник', 'двоюродный',
        'братов', 'онук', 'удов', 'гоподар', 'дядина', 'братов племянник', 'дячок', 'зать', 
        'плеянник', 'ученик', 'иконописец', 'голова', 'племяннкк', 'повар', 'сапожник'
    ]
    female_keywords = [
        'жена', 'дочь', 'вдова', 'невестка', 'мать', 'сестра', 'кухарка', 'девушка', 'теща', 'сестра',
        'вдова', 'племянница', 'доч', 
    ]

    if any(k in status for k in male_keywords):
        return 'm'
    elif any(k in status for k in female_keywords):
        return 'f'
    else:
        return pd.NA

df2_2_base_processed['Sex'] = df2_2_base_processed['Родиний статус'].apply(guess_sex)
df2_2_base_processed[df2_2_base_processed['Sex'].isna()]
Out[18]:
2 Аркуш ID наскрізна ID домогосподарство Двори джерело Чоловіки наскрізне Жінки наскрізне Чоловіки джерело Жінки джерело Імʼя По-батькові ... Родиний статус Категорія Клас Соціальний статус Вік Був на сповіді Не був Не був за малолітством Age Sex
248 NaN 249 NaN NaN 126 NaN 126 NaN Николай Григорьев ... NaN NaN NaN военные 39 х NaN NaN 39.0 <NA>
530 NaN 531 NaN NaN 267 NaN 267 NaN Андрей Стефанов ... ? NaN NaN военные 30 х NaN NaN 30.0 <NA>
607 NaN 608 NaN NaN NaN 302 NaN 302 Устина Мойсеева ... NaN NaN NaN военные 49 х NaN NaN 49.0 <NA>
1596 NaN 1597 NaN NaN 826 NaN 238 NaN Лукьян NaN ... NaN NaN NaN бездворные 14 х NaN NaN 14.0 <NA>

4 rows × 21 columns

In [19]:
#пусті клітинки які залишились заповнюємо вручну орієнтуючить на імена
df2_2_base_processed.loc[248, 'Sex'] = 'm'
df2_2_base_processed.loc[530, 'Sex'] = 'm'
df2_2_base_processed.loc[607, 'Sex'] = 'f'
df2_2_base_processed.loc[1596, 'Sex'] = 'm'

df2_2_base_processed[df2_2_base_processed['Sex'].isna()]
Out[19]:
2 Аркуш ID наскрізна ID домогосподарство Двори джерело Чоловіки наскрізне Жінки наскрізне Чоловіки джерело Жінки джерело Імʼя По-батькові ... Родиний статус Категорія Клас Соціальний статус Вік Був на сповіді Не був Не був за малолітством Age Sex

0 rows × 21 columns

In [20]:
#найдоречнішим, на мою думку,є створити вікову піраміду із обома роками
df1 = df2_2_base_processed.copy()
df2 = pd.merge(df2_1_people_with_age[['Age']], df2_1_people[['Sex']], 
               left_index=True, right_index=True, how='inner')

def create_age_groups(age):
    if age < 10:
        return '0-9'
    elif age < 20:
        return '10-19'
    elif age < 30:
        return '20-29'
    elif age < 40:
        return '30-39'
    elif age < 50:
        return '40-49'
    elif age < 60:
        return '50-59'
    elif age < 70:
        return '60-69'
    else:
        return '70+'

def process_dataset(df):
    df['age_group'] = df['Age'].apply(create_age_groups)
    grouped = df.groupby(['age_group', 'Sex']).size().unstack(fill_value=0)
    
    age_order = ['0-9', '10-19', '20-29', '30-39', '40-49', '50-59', '60-69', '70+']
    grouped = grouped.reindex(age_order)
    
    females = grouped.get('f', pd.Series(0, index=grouped.index))
    males = grouped.get('m', pd.Series(0, index=grouped.index))
    
    return females, males, age_order

females1, males1, age_order = process_dataset(df1)
females2, males2, age_order = process_dataset(df2)

fig, ax = plt.subplots(1, 1, figsize=(14, 10))

y_pos = np.arange(len(age_order))
bar_height = 0.35

# 1778 рік 
ax.barh(y_pos - bar_height/2, -males1, height=bar_height, 
        label='Чоловіки 1778', color='#1f4e79', alpha=0.8)
ax.barh(y_pos - bar_height/2, females1, height=bar_height, 
        label='Жінки 1778', color='#a61e1e', alpha=0.8)

# 1897 рік
ax.barh(y_pos + bar_height/2, -males2, height=bar_height, 
        label='Чоловіки 1897', color='#4472C4', alpha=0.7)
ax.barh(y_pos + bar_height/2, females2, height=bar_height, 
        label='Жінки 1897', color='#E15759', alpha=0.7)

ax.set_yticks(y_pos)
ax.set_yticklabels(age_order)
ax.set_xlabel('Кількість населення', fontsize=12)
ax.set_ylabel('Вікові групи', fontsize=12)
ax.set_title('Порівняння статево-вікової структури населення 1778 та 1897 років', 
             fontsize=14, fontweight='bold', pad=20)

max_val = max(males1.max(), females1.max(), males2.max(), females2.max())
ax.set_xlim(-max_val * 1.2, max_val * 1.2)

#фіксимо негативні значення зліва
ticks = ax.get_xticks()
ax.set_xticklabels([abs(int(tick)) for tick in ticks])

ax.axvline(x=0, color='black', linewidth=0.8)

ax.legend(loc='upper right', fontsize=10)

min_threshold = max_val * 0.05 
#числа на малих стовбцях погано зчитуються

for i, (m1, f1, m2, f2) in enumerate(zip(males1, females1, males2, females2)):
    # 1778 рік
    if m1 > min_threshold:
        ax.text(-m1/2, i - bar_height/2, str(m1), ha='center', va='center', 
               fontweight='bold', fontsize=8, color='white')
    if f1 > min_threshold:
        ax.text(f1/2, i - bar_height/2, str(f1), ha='center', va='center', 
               fontweight='bold', fontsize=8, color='white')
    
    # 1897 рік
    if m2 > min_threshold:
        ax.text(-m2/2, i + bar_height/2, str(m2), ha='center', va='center', 
               fontweight='bold', fontsize=8, color='white')
    if f2 > min_threshold:
        ax.text(f2/2, i + bar_height/2, str(f2), ha='center', va='center', 
               fontweight='bold', fontsize=8, color='white')

ax.grid(axis='x', alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

plt.tight_layout()
total_1778 = males1.sum() + females1.sum()
total_1897 = males2.sum() + females2.sum()

info_text = f"""Загальна кількість населення:
• 1778 рік: {total_1778:,} осіб
• 1897 рік: {total_1897:,} осіб

Розподіл за статтю:
1778 рік:
• Чоловіки: {males1.sum():,} ({(males1.sum() / total_1778 * 100):.1f}%)
• Жінки: {females1.sum():,} ({(females1.sum() / total_1778 * 100):.1f}%)

1897 рік:
• Чоловіки: {males2.sum():,} ({(males2.sum() / total_1897 * 100):.1f}%)
• Жінки: {females2.sum():,} ({(females2.sum() / total_1897 * 100):.1f}%)"""

ax = fig.gca()
ax.text(0.02, 0.98, info_text, transform=ax.transAxes, fontsize=9,
        verticalalignment='top', horizontalalignment='left',
        bbox=dict(boxstyle='round,pad=0.5', facecolor='lightgray', alpha=0.8))

plt.show()
/tmp/ipykernel_28230/2930493881.py:68: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels([abs(int(tick)) for tick in ticks])
No description has been provided for this image
In [21]:
'''
з цієї піраміди можна зробити такі висновки: 
-серед населення 0-9 та 40+ років відносна кількість жінок зросла 
-серед населення віком 10-19, 20-29 та 30-39 років відносність не зазнала суттєвих змін
-з роками суттєво збільшилась кількість людей 60+ обох статей
-кількість населення не зазнала великих змін, але відносна кількість жінок збільшилась
-серед населення 60+ досі переважають чоловіки
'''
Out[21]:
'\nз цієї піраміди можна зробити такі висновки: \n-серед населення 0-9 та 40+ років відносна кількість жінок зросла \n-серед населення віком 10-19, 20-29 та 30-39 років відносність не зазнала суттєвих змін\n-з роками суттєво збільшилась кількість людей 60+ обох статей\n-кількість населення не зазнала великих змін, але відносна кількість жінок збільшилась\n-серед населення 60+ досі переважають чоловіки\n'
In [22]:
#Структура і розміри родин; як це змінилось за сто років

#датасет 1
#розміри сім'ї
df2_1_lyst.rename(columns={'Всего наличного мужеского населения': 'Males count'}, inplace=True)
df2_1_lyst.rename(columns={'Всего наличнаго женского населения': 'Females count'}, inplace=True)
df2_1_lyst.columns
#для кожного ID господарства рахуємо кількість людей

#перевірка пустих значень
df2_1_lyst[df2_1_lyst['Males count'].isna()]
Out[22]:
Кто заполнял базу List ID Link ID Страница ID Домохозяйствао Губерния Уезд Волость Село/деревня Переписной участок ... Чем крыто Males count Females count Постоянно живущаго М Постоянно живущаго Ж Некрестьянсокго сословия М Некрестьянского сословия Ж Приписанного здесь М Приписанного здесь Ж Подпись счетчика
216 Podhorna F.132.Op.1.Spr.1343 NaN 455_456 217 NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

1 rows × 25 columns

In [23]:
#перевірка цього господарства в іншій таблиці з людьми
df2_1_people[df2_1_people['ID Домохозяйствао'] == 217]
Out[23]:
Кто заполнял базу ID строки в базе List ID Link ID Домохозяйствао ID жилец ФИО Sex Глава хозяйства и глава семьи Age ... Здесь ли обыкновенно проживает Отметка об отсуствии Вероисповедание Родной язык Умеет ли читать Обучение Профессия главное Профессия вспомогательное Положение по воинской повинности Примітки
1281 Podhorna 1282.0 F.132.Op.1.Spr.1343. Арк.455-456 https://www.familysearch.org/ark:/61903/3:1:3Q... 217.0 1.0 Vorobej Evdokim Andreev m husband 65 ... 1 NaN orthodox ukr 0.0 NaN farmer NaN NaN NaN
1282 Podhorna 1283.0 F.132.Op.1.Spr.1343. Арк.455-456 https://www.familysearch.org/ark:/61903/3:1:3Q... 217.0 2.0 Vorobej Marfa Aleksandrova f wife 65 ... 1 NaN orthodox ukr 0.0 NaN farmer with husband NaN NaN NaN
1283 Podhorna 1284.0 F.132.Op.1.Spr.1343. Арк.455-456 https://www.familysearch.org/ark:/61903/3:1:3Q... 217.0 3.0 Vorobej Grigorij Evdokimov m son 22 ... 1 NaN orthodox ukr 0.0 NaN farmer with father NaN NaN NaN
1284 Podhorna 1285.0 F.132.Op.1.Spr.1343. Арк.455-456 https://www.familysearch.org/ark:/61903/3:1:3Q... 217.0 4.0 Vorobej Elisaveta Evdokimova f daughter 19 ... 1 NaN orthodox ukr 0.0 NaN farmer with father NaN NaN NaN
1285 Podhorna 1286.0 F.132.Op.1.Spr.1343. Арк.455-456 https://www.familysearch.org/ark:/61903/3:1:3Q... 217.0 5.0 Vorobej Anna Evdokimova f daughter 27 ... 1 NaN orthodox ukr 0.0 NaN farmer with father NaN NaN NaN
1286 Podhorna 1287.0 F.132.Op.1.Spr.1343. Арк.455-456 https://www.familysearch.org/ark:/61903/3:1:3Q... 217.0 6.0 Vorobej Agafiya doch' devicy f granddaughter/extramarital daughter of #5 1 ... 1 NaN orthodox ukr 0.0 NaN with mother NaN NaN NaN

6 rows × 25 columns

In [24]:
df2_1_lyst.loc[216, 'Males count'] = 2
df2_1_lyst.loc[216, 'Females count'] = 4
#вручну вводимо кількість
In [25]:
df2_1_lyst['People count'] = df2_1_lyst['Males count'] + df2_1_lyst['Females count']
df2_1_lyst['People count'].describe()
Out[25]:
count    261.000000
mean       5.455939
std        2.499803
min        1.000000
25%        4.000000
50%        5.000000
75%        7.000000
max       18.000000
Name: People count, dtype: float64
In [26]:
df2_1_lyst['People count'].quantile(0.99)
Out[26]:
np.float64(12.0)
In [27]:
#буде доречно логарифмувати, оскільки родин із більш, ніж 12 людьми 1%
df2_1_lyst['log_people'] = np.log(df2_1_lyst['People count'])
In [28]:
#датасет 2 

#додаємо стовпець із інформацією про розміри сімей
mask = df2_2_base_processed['ID домогосподарство'].notna()

filled_idxs = df2_2_base_processed.index[mask].tolist() + [len(df2_2_base_processed)] 
df2_2_base_processed['People count'] = np.nan

for i in range(len(filled_idxs) - 1):
    start = filled_idxs[i]
    end = filled_idxs[i + 1]
    df2_2_base_processed.at[start, 'People count'] = end - start
In [29]:
df2_2_base_processed['People count'].describe()
Out[29]:
count    105.000000
mean      15.209524
std       11.488870
min        2.000000
25%        9.000000
50%       12.000000
75%       18.000000
max       71.000000
Name: People count, dtype: float64
In [30]:
#це також логарифмуємо
df2_2_base_processed['log_people'] = np.log(df2_2_base_processed['People count'])
In [31]:
people_count_1 = df2_1_lyst['log_people'].dropna()
people_count_2 = df2_2_base_processed['log_people'].dropna()

plt.figure(figsize=(10, 6))

plt.hist(people_count_2, bins=30, alpha=0.7, label='1778 р.', color='red')
plt.hist(people_count_1, bins=30, alpha=0.7, label='1897 р.', color='blue')

plt.xlabel('Кількість людей (log)')
plt.ylabel('Частота')
plt.title('Гістограма розмірів сімей')
plt.legend()
plt.grid(True, alpha=0.3)

plt.show()
No description has been provided for this image
In [32]:
#тепер можна перейти до структур родин
#всі дані потрібно об'єднати в 1 датафрейм та залишити лише необхідне

dataset1 = {
    'козаки': pd.read_csv('2.1.1_Структура родини Дунаєць_1897.xlsx - Cossacks.csv'),
    'духовництво': pd.read_csv('2.1.1_Структура родини Дунаєць_1897.xlsx - Dukhovnogo.csv'),
    'селянські власники': pd.read_csv('2.1.1_Структура родини Дунаєць_1897.xlsx - Peasant-owner(1).csv'),
    'шляхта': pd.read_csv('2.1.1_Структура родини Дунаєць_1897.xlsx - Nobles+imenuyushchijsya dvoryan.csv'),

}

dataset2 = {
    'козаки': pd.read_csv('2.2.2_Структура родини Дунаєць_1778.xlsx - Военные.csv'),
    'духовництво': pd.read_csv('2.2.2_Структура родини Дунаєць_1778.xlsx - Духовные(1).csv'),
    'селянські власники': pd.read_csv('2.2.2_Структура родини Дунаєць_1778.xlsx - Посполитые+бездворные(1).csv'),

}

classes = ['козаки', 'духовництво', 'селянські власники', 'шляхта']
all_data = []
In [33]:
for class_name in classes:
    if class_name in dataset1:
        df1 = dataset1[class_name].copy()
        df1['dataset'] = '1897'
        df1['class'] = class_name
        all_data.append(df1)
    if dataset2.get(class_name) is not None:
        df2 = dataset2[class_name].copy()
        df2['dataset'] = '1778'
        df2['class'] = class_name
        all_data.append(df2)

df = pd.concat(all_data, ignore_index=True)
In [34]:
df = df.drop(['Кількість', '%', '% кожного класу', 'Клас'], axis=1)
In [35]:
#тепер потрібно підіграти кількість населення у категоріях у рядок із кожною категорією
#класи я не буду враховувати у візуалізації, оскільки тоді вона буде перевантаженою
category_indices = df.index[df['Категорія'].notna()].tolist()

for i, idx in enumerate(category_indices):
    next_idx = category_indices[i+1] if i+1 < len(category_indices) else len(df)
    sub_df = df.loc[idx+1:next_idx-1, 'Усього по категоріях']
    
    value = sub_df[sub_df.notna()].first_valid_index()
    if value is not None:
        df.at[idx, 'Усього по категоріях'] = df.at[value, 'Усього по категоріях']

df = df[df['Категорія'].notna()].reset_index(drop=True)
df_filtered = df[df['Категорія'].str.lower().str.strip() != 'усього'].copy()
In [36]:
grouped = df_filtered.groupby(['Категорія', 'dataset', 'class'])['Усього по категоріях'].sum().reset_index()


#створюємо таблицю де розкладаємо кількість осіб 'Усього по категоріях' по категоріях, щоб кожен клас був окремим стовпцем
pivot = grouped.pivot_table(index=['Категорія', 'dataset'], 
                             columns='class', 
                             values='Усього по категоріях', 
                             fill_value=0).reset_index()

pivot = pivot.sort_values(['Категорія', 'dataset'])

numeric_columns = pivot.columns[2:]
for col in numeric_columns:
    pivot[col] = pd.to_numeric(pivot[col], errors='coerce').fillna(0)


categories = pivot['Категорія'].dropna().unique()
years = pivot['dataset'].dropna().unique()
#будуємо графік
fig, ax = plt.subplots(figsize=(16, 10))

n_categories = len(categories)
n_years = len(years)
bar_width = 0.35
group_width = bar_width * n_years
group_spacing = 0.3

colors = ['pink', 'lightblue', 'wheat', 'violet']

#правильне розташування
x_positions = np.arange(n_categories) * (group_width + group_spacing)

#малює стовпчикові діаграми для кожного року, посуваючи їх по осі x та stacked bar chart за категоріями
for i, year in enumerate(years): 
    year_data = pivot[pivot['dataset'] == year]
    x_pos = x_positions + i * bar_width
        
    bottom = np.zeros(len(year_data))
    for j, class_col in enumerate(numeric_columns):
        values = year_data[class_col].values
        ax.bar(x_pos, values, bar_width, bottom=bottom,
                color=colors[j % len(colors)], alpha=0.8, 
                label=class_col if i == 0 else "") 
        bottom += values

ax.set_ylabel('Кількість')
ax.set_title('Сімейні структури у 1897 та 1778 роках')

y_max = pivot[numeric_columns].sum(axis=1).max()
ax.set_ylim(0, y_max * 1.1)

ax.set_xticks(x_positions + bar_width / 2)
ax.set_xticklabels(categories)

max_value = pivot[numeric_columns].sum(axis=1).max()
for i, year in enumerate(years):
    for j, category in enumerate(categories):
        ax.text(x_positions[j] + i * bar_width, -5, 
                str(year), ha='center', va='top', fontsize=9)

handles, labels = ax.get_legend_handles_labels()
by_label = dict(zip(labels, handles))
ax.legend(by_label.values(), by_label.keys(), title='Клас', loc='upper left')

plt.tight_layout()
plt.show()
No description has been provided for this image
In [37]:
'''Із отриманих візуалізацій можна зробити такі висновки:
-за 100 років розміри сімей відчутно зменшилися, що повпливало і на структури сімей
-у мультифокальних сім'ях збільшилась частка козаків (військових) та з'явилась шляхта
-кількість нуклеарних родин стрімко зросла, але частка духовенства у ній зменшилась
-кількість розширених родин також зросла
-кількість самотніх осіб незначно збільшилась
'''
Out[37]:
"Із отриманих візуалізацій можна зробити такі висновки:\n-за 100 років розміри сімей відчутно зменшилися, що повпливало і на структури сімей\n-у мультифокальних сім'ях збільшилась частка козаків (військових) та з'явилась шляхта\n-кількість нуклеарних родин стрімко зросла, але частка духовенства у ній зменшилась\n-кількість розширених родин також зросла\n-кількість самотніх осіб незначно збільшилась\n"
In [38]:
#Які прізвища чи родини з обох баз можна повʼязати

#1 датафрейм
df1 = df2_1_people

#почнемо із розділення колонки 'ФИО' на окремі елементи

split_pib = df1['ФИО'].str.strip().str.split(' ', n=2, expand=True)
split_pib.columns = ['Surname', 'Name', 'Patronymic']

df1 = df1.join(split_pib)
df1[['Surname', 'Name', 'Patronymic']] = df1[['Surname', 'Name', 'Patronymic']].apply(lambda col: col.str.lower().str.strip())
In [39]:
df1[df1['Surname'].isna()]
#аналогічно з ситуацією з розміром родини і віком, перевіряємо в іншому датасеті це господарство
Out[39]:
Кто заполнял базу ID строки в базе List ID Link ID Домохозяйствао ID жилец ФИО Sex Глава хозяйства и глава семьи Age ... Родной язык Умеет ли читать Обучение Профессия главное Профессия вспомогательное Положение по воинской повинности Примітки Surname Name Patronymic
987 Podhorna 988.0 F.132.Op.1.Spr.1343. Арк.355-356 NaN 169.0 NaN NaN m NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
988 Podhorna 989.0 F.132.Op.1.Spr.1343. Арк.355-356 NaN 169.0 NaN NaN f NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
989 Podhorna 990.0 F.132.Op.1.Spr.1343. Арк.355-356 NaN 169.0 NaN NaN f NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

3 rows × 28 columns

In [40]:
df1_merged = pd.merge(df1, df2_1_lyst, on='ID Домохозяйствао')
df1_merged.loc[df1_merged['ФИО'].isna(), 'Хозяин']
Out[40]:
987    Ivan Samuilov Cygikal
988    Ivan Samuilov Cygikal
989    Ivan Samuilov Cygikal
Name: Хозяин, dtype: object
In [41]:
#припустимо, що у всіх в родині прізвище Cygikal
df1.loc[[987, 988, 989], 'Surname'] = 'cygikal'
In [42]:
#2 датафрейм
df2 =df2_2_base_processed
In [43]:
df2['Прізвище'] = df2['Прізвище'].replace('', pd.NA)
df2['Прізвище'] = df2['Прізвище'].ffill()
#заповнила пусті клітинки прізвищами господарів
In [44]:
def transliterate_to_latin(text):
    return translit(text, 'ru', reversed=True).lower().strip()

df2['Surname_latin'] = df2['Прізвище'].apply(transliterate_to_latin)
In [45]:
#для виявлення схожих прізвищ використаємо rapidfuzz
surnames_1897 = df1['Surname'].dropna().unique()
surnames_1778 = df2['Surname_latin'].dropna().unique()

threshold = 80
#це доволі оптимальне значення мінімальної схожості для незначних змін у прізвищах
matches = []

for surname in surnames_1897:
    match = process.extractOne(surname, surnames_1778, scorer=fuzz.ratio)
    if match and match[1] >= threshold:
        matches.append({'surname_1897': surname, 'surname_1778': match[0], 'similarity': match[1]})

matches_df = pd.DataFrame(matches)
In [46]:
heatmap = alt.Chart(matches_df).mark_rect().encode(
    x=alt.X('surname_1897:N', title='Прізвища з 1897', sort=alt.EncodingSortField(field="surname_1897", order='ascending')),
    y=alt.Y('surname_1778:N', title='Прізвища з 1778', sort=alt.EncodingSortField(field="surname_1778", order='ascending')),
    color=alt.Color('similarity:Q', scale=alt.Scale(scheme='blues'), legend=alt.Legend(title='Схожість')),
    tooltip=['surname_1897', 'surname_1778', 'similarity']
).properties(
    title='Теплова карта схожості прізвищ 1897 та 1778 років',
    width=800,
    height=800
)

heatmap
Out[46]:
In [47]:
'''Ця heatmap дозволяє побачити схожість наявних прізвищ і проаналізувати зв'язок між ними
-найтемніші відтінки свідчать про однаковість прізвищ, тому можна стверджувати, що вони належали одній родині
-такі призвища, як, наприклад, Науменко і Науменкова можна пов'язати і аргументувати зміну закінчення історичним контекстом - русифікацією прізвищ
-а, наприклад, poznyak та poznjak відмінними трансітераціями
-інші прізвища можна пояснити помилками в записі'''
Out[47]:
"Ця heatmap дозволяє побачити схожість наявних прізвищ і проаналізувати зв'язок між ними\n-найтемніші відтінки свідчать про однаковість прізвищ, тому можна стверджувати, що вони належали одній родині\n-такі призвища, як, наприклад, Науменко і Науменкова можна пов'язати і аргументувати зміну закінчення історичним контекстом - русифікацією прізвищ\n-а, наприклад, poznyak та poznjak відмінними трансітераціями\n-інші прізвища можна пояснити помилками в записі"